import os
import wx
import re

import app.util

from . import flasher_xrc

class _Dfu(object):
    """dfu-util executable wrapper."""

    _proc = None
    _gotSyms = 0
    _downloadSym = "#"
    _downloadSymCount = 50
    dfuOutput = ""
    _form = None
    _foundDevRe = re.compile(r"Found\s+Runtime:\s+"\
        r"\[(?P<vendor>0x1d50):(?P<product>0x5119)\]\s+"\
        r"devnum=(?P<devnum>\d+),\s+cfg=(?P<cfg>\d+),\s+"\
        r"intf=(?P<intf>\d+),\s+alt=(?P<alt>\d+),\s+"\
        r'name="(?P<name>USB Device Firmware Upgrade)"')
    # Don't you just love regexps?

    def __init__(self, appRoot, form, dfuData, doReset):
        _dfu = app.util.files.findExecutableFile("dfu-util")
        _args = [
            "-a", dfuData["partition"],
            "--device", "%s:%s" % (dfuData["vendor"], dfuData["product"]),
        ]
        if doReset:
            _args.append("--reset")
        _args.extend(("--download", dfuData["file"]))
        _args = app.util.files.escapeAllArgs(_args)
        _cmd = _dfu + " " + " ".join(_args)
        appRoot.log_debug("Calling %r" % _cmd)
        self._app = appRoot
        self._proc = app.util.subprocess.runCommandNonblocking(_cmd)
        self._msg = "flashing %s..." % dfuData["partition"]
        self._form = form
        self._data = dfuData

    def update(self):
        """Update this object with new data from dfu-util process"""
        if self.isCompleted():
            return
        _data = self._proc.stdout.read(100)
        self.dfuOutput += _data
        self._gotSyms += _data.count(self._downloadSym)
        self.setFormData()

    def setFormData(self):
        """Update parent form controls. (Pretty progressbars, etc...)"""
        _percent = (1.0 * self._gotSyms) / self._downloadSymCount
        self._form.progressStatus.SetLabel(
            self._msg + " (" + str(int(100 * _percent)) + "%)")
        _gPart = 1.0 / self._data["total"]
        _gaugePos = _gPart * self._data["pos"] + _percent * _gPart
        self._form.progressGauge.SetValue(int(100 * _gaugePos))

    def isCompleted(self):
        """Return True if underlying process has ended."""
        return self._proc.poll() is not None

    def getRetcode(self):
        return self._proc.returncode

    @classmethod
    def getUpgradable(cls, appRoot):
        """Return upgradable (via dfu-util) device list."""
        _dfu = app.util.files.findExecutableFile("dfu-util")
        _cmd = " ".join((_dfu, "-l"))
        appRoot.log_debug("Executing %r" % _cmd)
        _proc = app.util.subprocess.runCommandBlocking(_cmd)
        _status = _proc.stdout.readAll()
        _rv = []
        _matcher = cls._foundDevRe.search(_status)
        while _matcher and _status:
            _status = _status[_matcher.end():]
            _rv.append(_matcher.groupdict())
            _matcher = cls._foundDevRe.search(_status)
        return _rv

# this is semi-constants. Do not event think about changing them.
STATE_FAIL = 0
STATE_OK = 1
STATE_WARN = 2

class Flasher(flasher_xrc.xrcflasherPanel, app.util.pluginBase.PluginMixin):
    """Main Neo flasher panel."""

    name = "Neo flasher"
    _app = None
    _imgFolder = app.util.path.getModulePath("app.plugins.Flasher.img")
    _dfuQueue = _dfuTimer = None
    _errorColor = "red"
    _warnColor = "#DE7A1F"
    _okColor = "green"

    _dfuPict = {
        "ok": wx.Bitmap(os.path.join(_imgFolder, "tick.png")),
        "fail": wx.Bitmap(os.path.join(_imgFolder, "cross.png")),
    }

    def __init__(self, parent, app):
        super(Flasher, self).__init__(parent)
        self.Bind(wx.EVT_TIMER, self.OnDfuCheck)
        self._dfuTimer = wx.Timer(self, id=1)
        self._dfuTimer.Start(500)
        self._mixinInit(parent, app)
        self._findErrors()

    def _findErrors(self):
        """Check system environment for possibility of upgrade."""
        self._checkDfuUtil()

    def _checkDfuUtil(self):
        """Check if dfu-util can be used."""
        _isOk = app.util.files.findExecutableFile("dfu-util")
        _mask = "dfu-util %s"
        if _isOk:
            _status = "found"
            _bitmap = "ok"
        else:
            _status = "not found"
            _bitmap = "fail"
        self.dfu_status_text.SetLabel(_mask % _status)
        self.dfu_status_pic.SetBitmap(self._dfuPict[_bitmap])
        self.Refresh()
        return _status

    def getUboot(self):
        """Return u-boot image file name(or None)"""
        if not self.chkUboot.IsChecked():
            return None
        _file = self.fileUboot.GetPath()
        if not os.path.isfile(_file):
            return None
        return _file

    def getKernel(self):
        """Return kernel image file name(or None)"""
        if not self.chkKernel.IsChecked():
            return None
        _file = self.fileKernel.GetPath()
        if not os.path.isfile(_file):
            return None
        return _file

    def getRootfs(self):
        """Return rootfs image file name(or None)"""
        if not self.chkRootfs.IsChecked():
            return None
        _file = self.fileRootfs.GetPath()
        if not os.path.isfile(_file):
            return None
        return _file

    def checkUboot(self):
        """Check u-boot flashing data, give user some hints about possible errors.

        Return flashing state as STATE_*.

        """
        if self.chkUboot.IsChecked():
            _file = self.getUboot()
            _isOk = STATE_OK if _file else STATE_FAIL
            # need to give warning on probably incorrect u-boot image
            if _isOk == STATE_OK:
                if os.path.splitext(_file)[1].lower() != ".bin":
                    _color = self._warnColor
                    _msg = "File extension is supposed to be '.bin'"
                    _isOk = STATE_WARN
                elif "uboot" not in os.path.basename(_file).lower():
                    _color = self._warnColor
                    _msg = "'uboot' not in file name"
                    _isOk = STATE_WARN
                else:
                    _color = self._okColor
                    _msg = "OK"
            else:
                _color = self._errorColor
                _msg = "Incorrect value"
        else:
            _isOk = STATE_OK
            _color = self._okColor
            _msg = "OK"
        self.ubootError.SetBackgroundColour(_color)
        self.ubootError.SetLabel(_msg)
        return _isOk

    def checkKernel(self):
        """Check kernel flashing data, give user some hints about possible errors.

        Return flashing state as STATE_*.

        """
        if self.chkKernel.IsChecked():
            _file = self.getKernel()
            _isOk = STATE_OK if _file else STATE_FAIL
            # need to give warning on probably incorrect u-boot image
            if _isOk == STATE_OK:
                _fname = os.path.basename(_file).lower()
                if "uimage" not in _fname and "u-image" not in _fname:
                    _color = self._warnColor
                    _msg = "'uimage' not in file name"
                    _isOk = STATE_WARN
                else:
                    _color = self._okColor
                    _msg = "OK"
            else:
                _color = self._errorColor
                _msg = "Incorrect value"
        else:
            _isOk = STATE_OK
            _color = "green"
            _msg = "OK"
        self.kernelError.SetBackgroundColour(_color)
        self.kernelError.SetLabel(_msg)
        return _isOk

    def checkRootfs(self):
        """Check rootfs flashing data, give user some hints about possible errors.

        Return flashing state as STATE_*.

        """
        if self.chkRootfs.IsChecked():
            _file = self.getRootfs()
            _isOk = STATE_OK if _file else STATE_FAIL
            # need to give warning on probably incorrect u-boot image
            if _isOk == STATE_OK:
                if os.path.splitext(_file)[1].lower() != ".jffs2":
                    _color = self._warnColor
                    _msg = "File extension is supposed to be '.jffs2'"
                    _isOk = STATE_WARN
                elif "rootfs" not in os.path.basename(_file).lower():
                    _color = self._warnColor
                    _msg = "'rootfs' not in file name"
                    _isOk = STATE_WARN
                else:
                    _color = self._okColor
                    _msg = "OK"
            else:
                _color = self._errorColor
                _msg = "Incorrect value"
        else:
            _isOk = STATE_OK
            _color = self._okColor
            _msg = "OK"
        self.rootfsError.SetBackgroundColour(_color)
        self.rootfsError.SetLabel(_msg)
        return _isOk

    def tryAllowFlash(self):
        """Try to enable 'Flash it' button."""
        _canAllow = (
            self.checkUboot() and \
            self.checkKernel() and \
            self.checkRootfs()
        ) and (
            self.getRootfs() or \
            self.getKernel() or \
            self.getUboot()
        ) and app.util.files.findExecutableFile("dfu-util")
        self.doFlash.Enable(bool(_canAllow))

    def doFlashing(self):
        """Start the flashing process.

        (Supposing that user was already given all possible warnings).
        """
        _upgradable = _Dfu.getUpgradable(self._app)
        _termReason = None
        if not _upgradable:
            _termReason = "Dfu-util couldn't found any device.\n\n" \
                "If you are sure that Freerunner is connected:\n" \
                "\tIf you're using Linux, check permissions.\n" \
                "\tIf you're using OSX, try to replug Freerunner."
        elif len(_upgradable) > 1:
            _termReason = "Several upgradable devices found.\n" \
                "Sorry, but can only upgrade one per flashing."
        if _termReason:
            self._app.log_info(_termReason + "(Flashing terminated).")
            _dlg = wx.MessageDialog(
                self, _termReason + "\n\nFlashing terminated.",
                ':-(', wx.ICON_INFORMATION | wx.OK
            )
            _dlg.ShowModal()
            _dlg.Destroy()
            return
        _dfuData = _upgradable.pop()
        _stats = (
            ("u-boot", self.getUboot()),
            ("kernel", self.getKernel()),
            ("rootfs", self.getRootfs()),
        )
        _queue = []
        _pos = 0
        for _el in _stats:
            if not _el[1]:
                # Filter not needed values
                continue
            self._app.log_debug(
                "Calling dfu-util for partition %r and file %r" % _el)
            _vals = _dfuData.copy()
            _vals.update(partition=_el[0], file=_el[1], pos=_pos)
            _queue.append(_vals)
            _pos += 1
        _total = len(_queue)
        [_el.update(total=_total) for _el in _queue]
        self._dfuQueue = _queue

#####
    def OnButton_dfu_util_refresh(self, evt):
        """Refresh the dfu-util executable state (aka found or not)."""
        self._checkDfuUtil()

    def OnCheckbox_chkUboot(self, evt):
        """User changed 'flash u-boot' checkbox."""
        self.fileUboot.Enable(evt.Checked())

    def OnCheckbox_chkKernel(self, evt):
        """User changed 'flash kernel' checkbox."""
        self.fileKernel.Enable(evt.Checked())

    def OnCheckbox_chkRootfs(self, evt):
        """User changed 'flash rootfs' checkbox."""
        self.fileRootfs.Enable(evt.Checked())

    def OnFilepicker_changed_fileUboot(self, evt):
        """User changed 'flash uboot' checkbox."""
        self.checkUboot()
        self.tryAllowFlash()

    def OnFilepicker_changed_fileKernel(self, evt):
        """User has set kernel partition image file."""
        self.checkKernel()
        self.tryAllowFlash()

    def OnFilepicker_changed_fileRootfs(self, evt):
        """User has set rootfs partition image file."""
        self.checkRootfs()
        self.tryAllowFlash()

    def OnDfuCheck(self, evt):
        """Event that occures by timer, update underlying dfu-util state info."""
        if not self._dfuQueue:
            self._app.lockMainWindow(False)
            self.progressStatus.SetLabel("Idle")
            return
        self._app.lockMainWindow(True)
        _el = self._dfuQueue[0]
        if "process" not in _el:
            _isLast = _el["pos"] == (_el["total"] - 1)
            _el["process"] = _Dfu(self._app, self, _el, _isLast)
        else:
            _el["process"].update()
            if _el["process"].isCompleted():
                _rc = _el["process"].getRetcode()
                self._dfuQueue.pop(0)
                if _rc != 0:
                    self._app.log_debug("Failed dfu-util data: %r" % _el)
                    self._app.log_debug("Program output: %r" % _el["process"].dfuOutput)
                    self._app.log_info("Dfu-util return code was %s" % _rc)
                    self._app.log_error("Flashing terminated.")
                    _dlg = wx.MessageDialog(
                        self, "Flashing has failed.\n" \
                            "If you're using OSX you can try to replug Freerunner.",
                        ':-(', wx.ICON_INFORMATION | wx.OK
                    )
                    _dlg.ShowModal()
                    _dlg.Destroy()

    def OnButton_doFlash(self, evt):
        """On 'Flash it' button press."""
        _stats = (
            self.checkRootfs(), self.checkKernel(), self.checkUboot())
        if not all(_stats):
            return
        if STATE_WARN in _stats:
            # show warning dialog
            _dlg = wx.MessageDialog(
                self, "Are you sure? (some files haven't passed all checks)",
                'Last confirmation.', wx.ICON_INFORMATION |
                wx.YES_NO | wx.NO_DEFAULT
            )
            _ans = _dlg.ShowModal()
            _dlg.Destroy()
            if _ans != wx.ID_YES:
                return
        self.doFlashing()


# vim: set sts=4 sw=4 et :
